# -*- coding: utf-8 -*-

from random import choice
from copy import deepcopy

from lxml import etree

"""
环境：
python  -3.6.8
lxml    -4.3.4

麻将下叫说明:
1.既然是下叫了，那无论如何手上的13张牌都只有两种花色，所以下述讨论建立在两种花色的基础上，在实际需要下叫时，可以将手中的实际两种花色映射到此接口
2.麻将牌的数值大小，实际上是一种相对位置关系，所以采用列表的索引来标示牌值，索引 0~8，牌值 1~9
3.手中13张牌的表示方法 [[1, 1, 1, 1, 1, 0, 0, 2, 0], [0, 0, 0, 0, 2, 2, 2, 0, 0]]
  外层列表：花色0的牌列表，花色1的牌列表
  花色0的牌列表：花色0的牌在 索引 0~8 各位置上牌的个数；个数为 0 代表该花色该位置没有牌；个数上限为 4 ；如果是清一色，那么一个花色的牌中所有位置牌的个数都为0
  手里有13张牌时，两种花色所有位置上的牌的个数 和 为 13

4.麻将胡牌中可能的牌组合为 AAA, ABC, AA 三种形式，将一种合理组合称为 valid_group，形如 (0, ((1,1),(2,1),(3,1)))，表示：花色0，在位置索引 1,2,3 处有个 顺子合理组合
5.麻将的胡牌状态为 XXX XXX XXX XXXX XX, XX XX XX XX XX XX XX XX，将一种胡牌状态称为 hu_status
6.麻将下叫状态为 XX XX XX XX XX XX A, XXX XXX XXX XXX A, XXX XXX XXX XX AB(AA)，将一种下叫状态 称为 potential_hu_status
7.将有潜力形成合理组合的 A, AB, AC, AA 称为潜在合理组合，也就是最终下叫的组合 potential_valid_group

8.对于杠牌，杠牌比普通牌多一张，所以四张当三张，总数还是做 13 张处理

执行流程：
1.手牌13张初始化，有两种方式，其一为从两种花色的完整牌中随机发 13 张牌，这样下叫的可能性小 [ func:licensing_tiles ]
  其二为预设一组14张胡牌，随机从中抽取一张，剩下13张做手牌，这样肯定下叫 [ func:draw_a_tile_from_hu_tiles ]
2.以合理组合为单位，递归枚举出13张手牌所有可能的状态，以树结构保存 [ func:caculate_valid_groups ]
3.根据书叶子节点标记，从所有组合状态树中抽取出 潜在胡牌（下叫）状态 [ func:extract_total_potential_hu_status ]
4.根据 合理组合 张数的分布规律，进一步提取出潜在胡牌（下叫）状态 [ func:extract_further_potential_hu_status ]
5.对潜在胡牌（下叫）状态去重 [ func:get_unique_potential_hu_status ]
6.为每种潜在胡牌（下叫）状态下可能的叫 [ func:xiajiao ]
7.对下的叫做有效性校验 [ func:verify_jiao ]
8.打印出所有的叫

-- 测试预览 -----------------------------------------------------------------------------
[path]>python majiang.py
14张胡牌 >  [[1, 1, 2, 1, 1, 0, 0, 2, 0], [0, 0, 0, 0, 2, 2, 2, 0, 0]]
13张手牌 >  [[1, 1, 2, 1, 0, 0, 0, 2, 0], [0, 0, 0, 0, 2, 2, 2, 0, 0]]
下叫：
(0, ((1, 1),)) -- 花色：0, 牌位置：1 --
(0, ((4, 1),)) -- 花色：0, 牌位置：4 --

[~path]>python majiang.py
14张胡牌 >  [[0, 0, 0, 0, 3, 0, 0, 2, 0], [0, 0, 3, 0, 3, 0, 3, 0, 0]]
13张手牌 >  [[0, 0, 0, 0, 3, 0, 0, 2, 0], [0, 0, 2, 0, 3, 0, 3, 0, 0]]
下叫：
(1, ((2, 1),)) -- 花色：1, 牌位置：2 --
(0, ((7, 1),)) -- 花色：0, 牌位置：7 --

[~path]>python majiang.py
14张胡牌 >  [[2, 0, 2, 0, 2, 0, 0, 2, 0], [0, 0, 2, 0, 2, 0, 2, 0, 0]]
13张手牌 >  [[2, 0, 2, 0, 2, 0, 0, 1, 0], [0, 0, 2, 0, 2, 0, 2, 0, 0]]
下叫：
(0, ((7, 1),)) -- 花色：0, 牌位置：7 --

[~path]>python majiang.py
36张全牌 >  [[4, 4, 4, 4, 4, 4, 4, 4, 4], [4, 4, 4, 4, 4, 4, 4, 4, 4]]
13张手牌 >  [[1, 1, 0, 0, 2, 0, 0, 1, 1], [1, 0, 2, 0, 0, 2, 1, 0, 1]]
无叫

"""

def caculate_valid_groups(given_tiles, element):
    """
    将13张牌，以所有可能的合理组合为单位，枚举出这 13 张牌所有可能的状态，利用树结构来存储所有的状态
    枚举方法：
    1.首先给定13张牌，一个根元素节点
    2.枚举出这13张牌所有可能的 合理组合，AAA, ABC, AA, 一张牌可重复用
    3.遍历这些合理组合
        4.遍历得到一个 合理组合
        4.将 该合理组合 添加为 提供的父节点的子节点
        5.深拷贝给定的 13 张牌
        6.从拷贝牌中消除 该合理组合
        7.递归本过程 提供消除后剩余的牌，提供该子节点

        递归终结点：直到从剩余的牌中枚举不出任何 合理组合；并根据剩余牌判断是否有潜力形成一个 合理组合，能则打标记确定
    """
    total_valid_groups = enum_total_valid_groups(given_tiles)
    if total_valid_groups:
        for valid_group in total_valid_groups:
            curr_tiles = deepcopy(given_tiles)
            curr_element = etree.SubElement(element, 'valid_group')
            curr_element.text = str(valid_group)
            remove_group_from_tiles(curr_tiles, valid_group)
            caculate_valid_groups(curr_tiles, curr_element)
    else:
        rs = check_tail_potential(given_tiles)
        if rs:
            curr_element = etree.SubElement(element, 'tail', attrib={'potential':'1'})
            if type(rs) is not bool:
                curr_element.text = str(rs)
        else:
            curr_element = etree.SubElement(element, 'tail', attrib={'potential':'0'})
            curr_element.text = str(given_tiles)

def enum_total_valid_groups(given_tiles):
    """
    根据提供的牌，枚举出所有的 合理组合，分花色，形如(0, ((1,1),(2,1),(3,1)))，表示：花色0，在位置索引 1,2,3 处有个 顺子合理组合
    合理组合 有 AAA, ABC, AA 三类
    """
    total_valid_groups = []
    for color_index, color_tiles in zip(range(len(given_tiles)), given_tiles):
        for address, tile_count in zip(range(len(color_tiles)), color_tiles):
            if tile_count >= 3:
                total_valid_groups.append((color_index, ((address, 3),)))
                total_valid_groups.append((color_index, ((address, 2),)))
            elif tile_count == 2:
                total_valid_groups.append((color_index, ((address, 2),)))
            
            if address<7 and (tile_count>0 and color_tiles[address+1]>0 and color_tiles[address+2]>0):
                total_valid_groups.append((color_index, ((address, 1),(address+1, 1),(address+2, 1))))
    return total_valid_groups

def remove_group_from_tiles(given_tiles, valid_group):
    """
    给定一些牌，从中消除一个合理组合
    """
    color_index = valid_group[0]
    group_tiles = valid_group[1]
    for tiles in group_tiles:
        address = tiles[0]
        tile_count = tiles[1]
        given_tiles[color_index][address] -= tile_count

def check_tail_potential(tail_tails):
    """
    提供最终剩余的牌，判断是否有潜力形成一个 合理组合，由上述 enum_total_valid_groups 枚举发可知，剩余的牌中一个位置上最多只能有一张牌

    如果两种花色中都有牌，肯定没潜力
    如果两种花色中都没有牌，XXX XXX XXX AA BB 状态，有潜力，返回 True
    只有一个花色中有牌，如果剩的牌数多余 2 张，没潜力
        如果剩的牌数只有 1 张 A，有潜力，返回该 潜在合理组合 如：(0, ((5, 1),))
        如果剩的牌数只有 2 张：
            两张牌间隔大于2，无潜力
            两张牌间隔为1, AB，有潜力，返回该 潜在合理组合，如：(0, ((2,1),(3,1)))
            两张牌间隔为2, AC，有潜力，返回该 潜在合理组合，如：(0, ((2,1),(4,1)))
    """
    color0_tiles_sum = sum(tail_tails[0])
    color1_tiles_sum = sum(tail_tails[1])
    if color0_tiles_sum and color1_tiles_sum :
        return
    elif color0_tiles_sum>0:
        color = 0
        color_tiles = tail_tails[0]
    elif color1_tiles_sum>0:
        color = 1
        color_tiles = tail_tails[1]
    else:
        return True
    
    if sum(color_tiles) > 2:
        return
    elif sum(color_tiles) == 1:
        return (color,((color_tiles.index(1), 1),))
    else:
        tile_1st_idx = color_tiles.index(1)
        tile_2nd_idx = color_tiles.index(1, tile_1st_idx+1)
        if tile_2nd_idx-tile_1st_idx <= 2:
            return (color, ((tile_1st_idx, 1), (tile_2nd_idx, 1)))
        else:
            return

def extract_total_potential_hu_status(root_element):
    """
    从牌状态数结构中提取宠有潜力胡牌的状态，形态状态列表。根据 潜力属性标记提取
    """
    total_potential_hu_status = []
    el_tail_total_potential_hu_status = root_element.xpath('//tail[@potential="1"]')
    for el_tail_potential_hu_status in el_tail_total_potential_hu_status:
        potential_hu_status = []
        get_potential_hu_status_valid_groups(el_tail_potential_hu_status, potential_hu_status)
        total_potential_hu_status.append(potential_hu_status)
    return total_potential_hu_status

def get_potential_hu_status_valid_groups(el_tail_potential, potential_hu_status):
    """选定一个潜力叶子节点后，在该路径上递归向上取出所有 合理组合"""
    group = el_tail_potential.text
    el_parent = el_tail_potential.getparent()
    if el_parent is not None:
        if group:
            potential_hu_status.insert(0, eval(group))
        get_potential_hu_status_valid_groups(el_parent, potential_hu_status)
    else:
        return

def extract_further_potential_hu_status(total_potential_hu_status):
    """
    一个潜在胡牌（下叫）状态，其 合理组合 的牌张数 分布规律为：
    XX XX XX XX XX XX A ------- (1, 6, 0) 一张的合理分组1个，两张的合理分组6个，三张的合理分组0个
    XXX XXX XXX XXX A ------- (1, 0, 4)
    XXX XXX XXX XX AB(AA) ------- (0, 2, 3)

    依此规律进一步提取出潜在胡牌组合
    """
    further_potential_hu_status = []
    hu_status_distribution = [[1,6,0],[1,0,4],[0,2,3]]
    for potential_hu_status in total_potential_hu_status:
        status_distribution = [0,0,0]
        for valid_group in potential_hu_status:
            count = sum(dict(valid_group[1]).values())
            status_distribution[count-1] += 1
        if status_distribution in hu_status_distribution:
            further_potential_hu_status.append(potential_hu_status)
    return further_potential_hu_status

def get_unique_potential_hu_status(further_potential_hu_status):
    """潜在下叫状态去重，待下叫的潜力组合牌还是放到尾部"""
    unique_potential_hu_status = set()
    for potential_hu_status in further_potential_hu_status:
        foo = potential_hu_status[0:-1]
        foo.sort()
        foo.append(potential_hu_status[-1])
        unique_potential_hu_status.add(tuple(foo))
    return unique_potential_hu_status

def xiajiao(unique_potential_hu_status):
    """为每种下叫状态，给定叫牌"""
    total_hu_status_jiao = []
    for potential_hu_status in unique_potential_hu_status:
        jiao_group = potential_hu_status[-1]
        tiles_kind = len(jiao_group[1])
        if tiles_kind == 1:
            if jiao_group[1][0][1] == 1:
                jiao = (jiao_group[0],  ((jiao_group[1][0][0], 1),))
                total_hu_status_jiao.append({'tiles': potential_hu_status, 'jiao':jiao})
            else:
                for valid_group in potential_hu_status:
                    if valid_group[1][0][1] == 2:
                        jiao = ((valid_group[0], ((valid_group[1][0][0], 1),)))
                        total_hu_status_jiao.append({'tiles': potential_hu_status, 'jiao':jiao})
        elif tiles_kind == 2:
            interval = jiao_group[1][1][0] - jiao_group[1][0][0]
            if interval == 1:
                jiao1 = (jiao_group[0], ((jiao_group[1][0][0]-1, 1),))
                jiao2 = (jiao_group[0], ((jiao_group[1][1][0]+1, 1),))
                total_hu_status_jiao.append({'tiles': potential_hu_status, 'jiao':jiao1})
                total_hu_status_jiao.append({'tiles': potential_hu_status, 'jiao':jiao2})
            elif interval == 2:
                jiao = (jiao_group[0], ((int((jiao_group[1][0][0] + jiao_group[1][1][0])/2), 1),))
                total_hu_status_jiao.append({'tiles': potential_hu_status, 'jiao':jiao})
    return total_hu_status_jiao

def verify_jiao(hand_tiles_13, total_hu_status_jiao):
    """检验叫牌的合理性，位置索引在 0~8 之间，叫牌加上手上该位置的牌数不能大于4"""
    valid_jiao = set()
    for hu_status_jiao in total_hu_status_jiao:
        jiao = hu_status_jiao.get('jiao')
        color = jiao[0]
        address = jiao[1][0][0]
        if address < 0 or address > 8:
            continue
        elif hand_tiles_13[color][address] + 1 > 4:
            continue
        else:
            valid_jiao.add(jiao)
    return valid_jiao

def licensing_tiles(total_tiles):
    """从两种完整花色的牌中随机发13张牌出来"""
    hand_tiles_13 = [[0 for _ in range(9)]for _ in range(2)]
    i = 0
    while i < 13:
        while True:
            color = choice(range(2))
            address = choice(range(9))
            if total_tiles[color][address] > 0:
                hand_tiles_13[color][address] += 1
                total_tiles[color][address] -= 1
                break
        i += 1
    return hand_tiles_13

def draw_a_tile_from_hu_tiles(hu_tiles_14):
    """从一把14张的胡牌中随机抽取一张出来，形成叫牌状态"""
    hand_tiles_13 = deepcopy(hu_tiles_14)
    while True:
        color = choice(range(2))
        address = choice(range(9))
        if hu_tiles_14[color][address] > 0:
            hand_tiles_13[color][address] -= 1
            return hand_tiles_13

def main():
    # 随机发13张两种花色的牌作为手牌
    # total_tiles = [[4 for _ in range(9)]for _ in range(2)]
    # print('36张全牌 > ', total_tiles)
    # hand_tiles_13 = licensing_tiles(total_tiles)
    
    # 从一把14张的胡牌中随机抽取一张出来，形成叫牌状态手牌
    # hu_tiles_14 = [[2, 0, 2, 0, 2, 0, 0, 2, 0], [0, 0, 2, 0, 2, 0, 2, 0, 0]]
    # hu_tiles_14 = [[0, 0, 0, 0, 3, 0, 0, 2, 0], [0, 0, 3, 0, 3, 0, 3, 0, 0]]
    hu_tiles_14 = [[1, 1, 2, 1, 1, 0, 0, 2, 0], [0, 0, 0, 0, 2, 2, 2, 0, 0]]
    print('14张胡牌 > ', hu_tiles_14)
    hand_tiles_13 = draw_a_tile_from_hu_tiles(hu_tiles_14)

    hand_tiles_13 = [[0,0,0,0,0,0,3,0,0],[0,0,1,1,1,2,2,1,2]]

    print('13张手牌 > ', hand_tiles_13)
    
    # total_valid_groups = enum_total_valid_groups(hand_tiles_13)
    # for i in total_valid_groups:
    #     print(i)
    
    # 以合理组合单位，枚举出这13张牌所有可能的状态，形成树结构
    root_element = etree.Element('root')
    caculate_valid_groups(hand_tiles_13, root_element)
    
    # 将此数结构输出到xml文档中，方便查看
    doc = etree.tostring(root_element, pretty_print=True, encoding='utf-8', xml_declaration=True)
    with open('majiang.xml', 'wb') as fp:
        fp.write(doc)
    
    # 从状态树中出去出所有的 潜在胡牌（下叫）状态
    total_potential_hu_status = extract_total_potential_hu_status(root_element)

    # 根据 合理组合 张数的分布规律，进一步提取出潜在胡牌（下叫）状态
    further_potential_hu_status = extract_further_potential_hu_status(total_potential_hu_status)

    # 对潜在胡牌（下叫）状态去重
    unique_potential_hu_status = get_unique_potential_hu_status(further_potential_hu_status)
    
    # 为每种潜在胡牌（下叫）状态下可能的叫
    total_hu_status_jiao = xiajiao(unique_potential_hu_status)

    # 对下的叫做有效性校验
    valid_jiao = verify_jiao(hand_tiles_13, total_hu_status_jiao)
    if valid_jiao:
        print('下叫：')
        for i in valid_jiao:
            print(i, '-- 花色：%s, 牌位置：%s --' % (i[0], i[1][0][0]))
    else:
        print('无叫')
    
    # -- debug --
    # with open('majiang_hu_status.text', 'a', encoding='utf-8') as fp:
    #     fp.write('----------------------------------------------------------------------\n')
    #     for hu_status_jiao in unique_potential_hu_status:
    #         fp.write(str(hu_status_jiao))
    #         fp.write('\n')

if __name__ == '__main__':
    main()
